/**********************************************************************
 * SailmiPIM - modular cross-platform feature Personal Information Manager Tools.
 * Copyright(C) 2006-2014  Georg Rudoy
 *
 * Distributed under the Boost Software License, Version 1.0.
 *(See accompanying file LICENSE or copy at https://www.boost.org/LICENSE_1_0.txt)
 **********************************************************************/

#include <algorithm>
#include <stdexcept>
#include <QMimeData>
#include <QUrl>
#include <QtDebug>
#include <util/sll/qtutil.h>
#include "mergemodel.h"

namespace SailmiPIM::Util
{
	MergeModel::MergeModel(QStringList headers, QObject *parent)
	: QAbstractItemModel(parent)
	, Headers_(std::move(headers))
	, Root_(std::make_shared<ModelItem>())
	{
	}

	int MergeModel::columnCount(const QModelIndex& index) const
	{
		if(!index.isValid())
			return Headers_.size();
		const auto& mapped = mapToSource(index);
		return mapped.model()->columnCount(mapped);
	}

	QVariant MergeModel::headerData(int column, Qt::Orientation orient, int role) const
	{
		if(orient != Qt::Horizontal || role != Qt::DisplayRole)
			return QVariant();

		return Headers_.at(column);
	}

	QVariant MergeModel::data(const QModelIndex& index, int role) const
	{
		if(!index.isValid())
			return QVariant();

		try
		{
			return mapToSource(index).data(role);
		}
		catch(const std::exception& e)
		{
			qWarning() << Q_FUNC_INFO
					<< e.what();
			return {};
		}
	}

	Qt::ItemFlags MergeModel::flags(const QModelIndex& index) const
	{
		try
		{
			return mapToSource(index).flags();
		}
		catch(const std::exception& e)
		{
			qWarning() << Q_FUNC_INFO
					<< e.what();
			return {};
		}
	}

	QModelIndex MergeModel::index(int row, int column, const QModelIndex& parent) const
	{
		if(!hasIndex(row, column, parent))
			return {};

		auto parentItem = parent.isValid() ?
				static_cast<ModelItem*>(parent.internalPointer()) :
				Root_.get();

		return createIndex(row, column, parentItem->EnsureChild(row));
	}

	QModelIndex MergeModel::parent(const QModelIndex& index) const
	{
		if(!index.isValid() || index.internalPointer() == Root_.get())
			return {};

		auto item = static_cast<ModelItem*>(index.internalPointer());
		auto parent = item->GetParent();
		if(parent == Root_)
			return {};

		return createIndex(parent->GetRow(), 0, parent.get());
	}

	int MergeModel::rowCount(const QModelIndex& parent) const
	{
		if(!parent.isValid())
			return Root_->GetRowCount();

		const auto item = static_cast<ModelItem*>(parent.internalPointer());
		return item->GetModel()->rowCount(item->GetIndex());
	}

	QStringList MergeModel::mimeTypes() const
	{
		QStringList result;
		for(const auto model : GetAllModels())
			for(const auto& type : model->mimeTypes())
				if(!result.contains(type))
					result << type;
		return result;
	}

	namespace
	{
		void Merge(QMimeData& out, const QMimeData& sub)
		{
			for(const auto& format : sub.formats())
				if(format != "text/uri-list"_ql && !out.hasFormat(format))
					out.setData(format, sub.data(format));

			out.setUrls(out.urls() + sub.urls());
		}
	}

	QMimeData* MergeModel::mimeData(const QModelIndexList& indexes) const
	{
		std::unique_ptr<QMimeData> result;

		for(const auto& index : indexes)
		{
			const auto& src = mapToSource(index);

			std::unique_ptr<QMimeData> subresult { src.model()->mimeData({ src }) };
			if(!subresult)
				continue;

			if(!result)
				result = std::move(subresult);
			else
				Merge(*result, *subresult);
		}

		return result.release();
	}

	QModelIndex MergeModel::mapFromSource(const QModelIndex& sourceIndex) const
	{
		if(!sourceIndex.isValid())
			return {};

		QList<QModelIndex> hier;
		auto parent = sourceIndex;
		while(parent.isValid())
		{
			hier.prepend(parent);
			parent = parent.parent();
		}

		auto currentItem = Root_;
		for(const auto& idx : hier)
		{
			currentItem = currentItem->FindChild(idx);
			if(!currentItem)
			{
				qWarning() << Q_FUNC_INFO
						<< "no next item for"
						<< idx
						<< hier;
				return {};
			}
		}

		return createIndex(currentItem->GetRow(), sourceIndex.column(), currentItem.get());
	}

	QModelIndex MergeModel::mapToSource(const QModelIndex& proxyIndex) const
	{
		const auto item = proxyIndex.isValid() ?
				static_cast<ModelItem*>(proxyIndex.internalPointer()) :
				Root_.get();

		const auto& srcIdx = item->GetIndex();
		return srcIdx.sibling(srcIdx.row(), proxyIndex.column());
	}

	void MergeModel::setSourceModel(QAbstractItemModel*)
	{
		throw std::runtime_error("You should not set source model via setSourceModel()");
	}

	void MergeModel::SetHeaders(const QStringList& headers)
	{
		Headers_ = headers;
	}

	void MergeModel::AddModel(QAbstractItemModel *model)
	{
		if(!model)
			return;

		Models_.push_back(model);
/*
		auto withModel = [this, model]<typename... Args>(void(MergeModel::*method)(QAbstractItemModel*, Args...))
		{
			return [this, model, method](Args... args) {(this->*method)(model, args...); };
		};

		connect(model,
				&QAbstractItemModel::dataChanged,
				this,
				[this](const QModelIndex& topLeft, const QModelIndex& bottomRight)
				{
					emit dataChanged(mapFromSource(topLeft), mapFromSource(bottomRight));
				});
		connect(model,
				&QAbstractItemModel::layoutAboutToBeChanged,
				this,
				withModel(&MergeModel::HandleModelAboutToBeReset));
		connect(model,
				&QAbstractItemModel::layoutChanged,
				this,
				withModel(&MergeModel::HandleModelReset));
		connect(model,
				&QAbstractItemModel::modelAboutToBeReset,
				this,
				withModel(&MergeModel::HandleModelAboutToBeReset));
		connect(model,
				&QAbstractItemModel::modelReset,
				this,
				withModel(&MergeModel::HandleModelReset));
		connect(model,
				&QAbstractItemModel::rowsAboutToBeInserted,
				this,
				withModel(&MergeModel::HandleRowsAboutToBeInserted));
		connect(model,
				&QAbstractItemModel::rowsAboutToBeRemoved,
				this,
				withModel(&MergeModel::HandleRowsAboutToBeRemoved));
		connect(model,
				&QAbstractItemModel::rowsInserted,
				this,
				withModel(&MergeModel::HandleRowsInserted));
		connect(model,
				&QAbstractItemModel::rowsRemoved,
				this,
				withModel(&MergeModel::HandleRowsRemoved));
*/
		if(const auto rc = model->rowCount())
		{
			beginInsertRows({}, rowCount({}), rowCount({}) + rc - 1);

			for(auto i = 0; i < rc; ++i)
				Root_->AppendChild(model, model->index(i, 0), Root_);

			endInsertRows();
		}
	}

	MergeModel::const_iterator MergeModel::FindModel(const QAbstractItemModel *model) const
	{
		return std::find(Models_.begin(), Models_.end(), model);
	}

	MergeModel::iterator MergeModel::FindModel(const QAbstractItemModel *model)
	{
		return std::find(Models_.begin(), Models_.end(), model);
	}

	void MergeModel::RemoveModel(QAbstractItemModel *model)
	{
		auto i = FindModel(model);

		if(i == Models_.end())
		{
			qWarning() << Q_FUNC_INFO << "not found model" << model;
			return;
		}

		for(auto r = Root_->begin(); r != Root_->end(); )
			if((*r)->GetModel() == model)
			{
				const auto idx = static_cast<int>(std::distance(Root_->begin(), r));

				beginRemoveRows({}, idx, idx);
				r = Root_->EraseChild(r);
				endRemoveRows();
			}
			else
				++r;

		Models_.erase(i);
	}

	size_t MergeModel::Size() const
	{
		return Models_.size();
	}

	int MergeModel::GetStartingRow(MergeModel::const_iterator it) const
	{
		int result = 0;
		for(auto i = Models_.begin(); i != it; ++i)
			result +=(*i)->rowCount({});
		return result;
	}

	int MergeModel::GetStartingRow(MergeModel::iterator it)
	{
		int result = 0;
		for(auto i = Models_.begin(); i != it; ++i)
			result +=(*i)->rowCount({});
		return result;
	}

	MergeModel::const_iterator MergeModel::GetModelForRow(int row, int *starting) const
	{
		const auto child = Root_->GetChild(row);
		const auto it = FindModel(child->GetModel());

		if(starting)
			*starting = GetStartingRow(it);

		return it;
	}

	MergeModel::iterator MergeModel::GetModelForRow(int row, int *starting)
	{
		const auto child = Root_->GetChild(row);
		const auto it = FindModel(child->GetModel());

		if(starting)
			*starting = GetStartingRow(it);

		return it;
	}

	QList<QAbstractItemModel*> MergeModel::GetAllModels() const
	{
		QList<QAbstractItemModel*> result;
		for(auto p : Models_)
			if(p)
				result << p.data();
		return result;
	}

	void MergeModel::HandleRowsAboutToBeInserted(QAbstractItemModel *model, const QModelIndex& parent, int first, int last)
	{
		const auto startingRow = parent.isValid() ?
				0 :
				GetStartingRow(FindModel(model));
		beginInsertRows(mapFromSource(parent),
				first + startingRow, last + startingRow);
	}

	void MergeModel::HandleRowsAboutToBeRemoved(QAbstractItemModel *model, const QModelIndex& parent, int first, int last)
	{
		const auto startingRow = parent.isValid() ?
				0 :
				GetStartingRow(FindModel(model));
		const auto mergedParent = mapFromSource(parent);
		beginRemoveRows(mergedParent, first + startingRow, last + startingRow);

		const auto rawItem = parent.isValid() ?
				static_cast<ModelItem*>(mergedParent.internalPointer()) :
				Root_.get();
		const auto& item = rawItem->shared_from_this();

		auto it = item->EraseChildren(item->begin() + startingRow + first,
				item->begin() + startingRow + last + 1);

		RemovalRefreshers_.push([=]() mutable
				{
					for( ; it != item->end() &&(*it)->GetModel() == model; ++it)
						(*it)->RefreshIndex(startingRow);
				});
	}

	void MergeModel::HandleRowsInserted(QAbstractItemModel *model, const QModelIndex& parent, int first, int last)
	{
		const auto startingRow = parent.isValid() ?
				0 :
				GetStartingRow(FindModel(model));

		const auto rawItem = parent.isValid() ?
				static_cast<ModelItem*>(mapFromSource(parent).internalPointer()) :
				Root_.get();
		const auto& item = rawItem->shared_from_this();

		for( ; first <= last; ++first)
		{
			const auto& srcIdx = model->index(first, 0, parent);
			item->InsertChild(startingRow + first, model, srcIdx, item);
		}

		++last;
		last += startingRow;

		for(int rc = item->GetRowCount(); last < rc; ++last)
		{
			const auto child = item->GetChild(last);
			if(child->GetModel() != model)
				break;

			child->RefreshIndex(startingRow);
		}

		endInsertRows();
	}

	void MergeModel::HandleRowsRemoved(QAbstractItemModel*, const QModelIndex&, int, int)
	{
		RemovalRefreshers_.pop()();
		endRemoveRows();
	}

	void MergeModel::HandleModelAboutToBeReset(QAbstractItemModel *model)
	{
		if(const auto rc = model->rowCount())
		{
			const auto startingRow = GetStartingRow(FindModel(model));
			beginRemoveRows({}, startingRow, rc + startingRow - 1);
			Root_->EraseChildren(Root_->begin() + startingRow, Root_->begin() + startingRow + rc);
			endRemoveRows();
		}
	}

	void MergeModel::HandleModelReset(QAbstractItemModel *model)
	{
		if(const auto rc = model->rowCount())
		{
			const auto startingRow = GetStartingRow(FindModel(model));

			beginInsertRows({}, startingRow, rc + startingRow - 1);

			for(int i = 0; i < rc; ++i)
				Root_->InsertChild(startingRow + i, model, model->index(i, 0, {}), Root_);

			endInsertRows();
		}
	}

	bool MergeModel::AcceptsRow(QAbstractItemModel*, int) const
	{
		DefaultAcceptsRowImpl_ = true;
		return true;
	}

	int MergeModel::RowCount(QAbstractItemModel *model) const
	{
		if(!model)
			return 0;

		int orig = model->rowCount();
		if(DefaultAcceptsRowImpl_)
			return orig;

		int result = 0;
		for(int i = 0; i < orig; ++i)
			result += AcceptsRow(model, i) ? 1 : 0;
		return result;
	}
}
